import { sleep } from '../../base/utils'
import {readdir, exists, readFile} from  "../../base/fs.async"
import { Ref } from 'vue'
import ChromeSerialSocket from '@beshell/conn-ts/ChromeSerialSocket'
const https = require('https')

if(window.runtime.platform=="nw.js") {
    var path: any = (window as any).require("path")
    var statSync: any = (window as any).require("fs").statSync
    var gui = (window as any).require('nw.gui')
    var { spawn } = (window as any).require('child_process')
}

;(window as any).burnBaudRate = 921600

export const rootdir = path.dirname((window as any).process.mainModule.filename)

let _firmdir = rootdir + path.sep + "assets" + path.sep + "firmware"
export const firmdir = path.format(path.parse(_firmdir))


export function openFirmwareDir() {
    gui.Shell.openItem(firmdir)
}

async function loadFirmwareJson(jsonpath:string, dirpath:string) {

    let jsoncontent = await readFile(jsonpath)
    if(!jsoncontent){
        console.log("file is empty", jsonpath)
        return
    }

    let json = JSON.parse(jsoncontent)

    if(!json.name) {
        console.log("no name property", jsonpath)
        return
    }

    if(!json.chips) {
        json.chips = ["unknow"]
    }
    
    if(json["write-options"] && !json["write-options"].argvs) {
        json["write-options"].argvs = []
    }

    json.bin.sort((a,b)=>{
        return a.address - b.address
    })

    if(json.bin && json.bin instanceof Array) {
        let mapBinFiles:any = {}
        for(let bin of json.bin){
            let fullpath = dirpath + path.sep + bin.path
            if(!mapBinFiles[bin.path]) {
                let item: any = {
                    enable: (bin.enable==undefined? true: (!!bin.enable)) ,
                    path: bin.path ,
                    fullpath,
                    size: bin.size,
                    modifytime: fileModifyTime(fullpath) ,
                    address: {}
                }
                if(bin.chiptype) {
                    item.chiptype = bin.chiptype
                }
                if(bin.flashsize) {
                    item.flashsize = bin.flashsize
                }
                mapBinFiles[bin.path] = item
            }
            mapBinFiles[bin.path].address[ (bin.flashsize || "*") ] = bin.address
        }
        json.bin = Object.values(mapBinFiles)
        // console.log(json.bin)
    }

    json.dirpath = dirpath

    console.log(json)

    return json

}


export function fileModifyTime(path:string) {
    try {
        let stat = statSync(path)
        return parseInt(stat.mtimeMs)
    } catch(e) {
        // console.error(e)
        return 0
    }
}

function makeVersion(versionString:string): number {
    let [a,b,c] = versionString.split('.')
    return (parseInt(a) << 24) | (parseInt(b) << 16) | ( parseInt(c)<<8 )
}

export async function loadFirmwares(selected: Ref<number>) {

    let lst: any[] = []
    let lastBeShellIndex = -1
    let lastBeShellVersion = -1

    let dirlst = await readdir(firmdir)
    if(!dirlst)
        return lst

    for(let dirname of dirlst) {

        let jsonpath = firmdir + "/" + dirname + "/firmware.json"
        if( !await exists(jsonpath)) {
            continue
        }

        try{

            let dirpath = firmdir + "/" + dirname

            let json: any = await loadFirmwareJson(jsonpath, dirpath)
            if(!json){
                continue
            }
            json.index = lst.length
            json.label = `${json.name}  版本:${json.version}`
            json.value = json.index
            json.grpkey = json.name.toLowerCase()

            lst.push(json)

            // if(json.name==originSelectJson.name && json.version==originSelectJson.version) {
            //     selectedFirmwareIndex.value = json.index
            // }

            if(json.name=="BeShell") {
                let version = makeVersion(json.version)
                if(version>lastBeShellVersion) {
                    lastBeShellVersion = version
                    lastBeShellIndex = json.index
                }
            }


        }
        catch(e) {
            console.error(e)
        }
    }
    
    // console.log("mapFirmwareGroups",mapFirmwareGroups)
    
    if(selected) {
        if(selected.value==-1) {
            if(lastBeShellIndex>-1) {
                selected.value = lastBeShellIndex
            }
            else {
                selected.value = 0
            }
        }
    }

    return lst
}




function pythonPath() {
    return rootdir+"/assets/tools/python/python.exe"
}
function pythonEnv() {
    return {
        // PYTHONPATH: `${rootdir}${path.sep}assets${path.sep}tools${path.sep}python${path.sep}Lib${path.sep}site-packages` ,
        // PATH: `${rootdir}${path.sep}assets${path.sep}tools${path.sep}python; ${process.env.PATH}`
    }
}

export function runEspToolProcess(argvs: any[], echo: ((d:string)=>void)|null=null, tool='esptool.py'): Promise<any> {

        let promise = runPython(rootdir+"/assets/tools/esptool/"+tool, argvs, echo)

        promise.catch(e=>{
            if(e) {
                echo&&echo(`烧写固件遇到了错误，错误码：${e}\r\n\r\n`)
            }
        })

        return promise
}

window.spawn = spawn
export function runPython(scriptPath: string, argvs: any[], echo: ((d:string)=>void)|null=null): Promise<any> {

    let data = ""
    let error = ""

    let pythenpath = pythonPath()
    argvs.unshift(scriptPath)
    
    // 添加 -u 参数来禁用Python输出缓冲
    argvs.unshift("-u")

    echo&&echo(pythenpath + " " + argvs.join(" ") + "\r\n")

    // console.log(pythenpath)
    console.log(pythonEnv())
    // return
    let process = spawn( pythenpath, argvs, {env:pythonEnv()})
    process.stdout.on("data", (_dat: any)=>{
        _dat = _dat.toString()
        console.log(_dat)
        echo&&echo(_dat) 
        data+= _dat
    })
    process.stderr.on("data", (data: any)=>{
        console.log("errrrrrrrrr:",error)
        data = data.toString()
        console.error(data)
        echo&&echo(data)
        error+= data
    })
    process.stderr.on("close", (code: any)=>{
        console.log("proc close",code)
    })

    let promise = new Promise(resolve=>{
        process.on("exit", async (code: any)=>{
            process = null            
            resolve(error? null: data)
        })
    })

    ; (promise as any).process = process
    return promise
}

export function watchUSB(callback: ((event:"+"|"-", path:string, vid:string, pid:string)=>void)|null=null): void {
    let promise: any = runPython(rootdir+"/assets/tools/usb-watch.py", [])

    // 移除重复的stdout监听器，因为runPython已经处理了stdout
    promise.process.stdout.on("data", (data: any)=>{
        // 解析USB设备变化事件
        if(callback) {
            const lines = data.toString().split('\n');
            for(const line of lines) {
                const trimmedLine = line.trim();
                if(trimmedLine.startsWith('+:') || trimmedLine.startsWith('-:')) {
                    const event = trimmedLine.charAt(0) as "+"|"-";
                    const parts = trimmedLine.substring(2).split(',');
                    if(parts.length >= 3) {
                        const path = parts[0];
                        const vid = parts[1];
                        const pid = parts[2];
                        callback(event, path, vid, pid);
                    }
                }
            }
        }
    })
}

export async function eraseFirmware(devicePath:string, echo: ((d:string)=>void)|null=null) {
    
    try {
        
        // 擦除
        echo&&echo("擦除flash ... \r\n\r\n")
        await runEspToolProcess([
            '--port', devicePath,
            // '--baud', (window as any).burnBaudRate,
            'erase_flash'
        ], echo)

        echo&&echo("完成 ["+new Date().toLocaleString()+"]\r\n\r\n")
        
        await sleep(100)

    }catch(e: any){
        console.error(e)
        echo&&echo(e.toString())
    }
}


export async function detectFlashSize(devicePath:string, echo: ((d:string)=>void)|null=null): Promise<[string,string|null,number,string|null]> {

    let data = await runEspToolProcess([
        '--port', devicePath, 
        // '--baud', (window as any).burnBaudRate,
        'flash_id'
    ], echo)


    if(!data) {
        return ['', null, 0, null]
    }

    let chip = null
    let chipType:any = ''
    let flashSize = 0
    let mac = null

// 
    let ret = data.match(/Detecting chip type... ([^\s\n\(\)]+)/i)
    if(ret) {
        chipType = ret[1].toLowerCase().replace(/\-/gm,'')
    }
    ret = data.match(/Chip is ([^\s\n\(\)]+)/i)
    console.log(ret)
    if(ret) {
        chip = ret[1]
    }

    ret = data.match(/MAC: ?([\w:]+)/)
    if(ret) {
        mac = ret[1]
    }
    
    ret = data.match(/flash size: (\d+)MB/i)
    if(ret) {
        flashSize = parseInt(ret[1])
    }

    // ESP32-PICO-D4 不输出 flash size , 但内置 4MB
    if(chip=='ESP32-PICO-D4') {
        flashSize = 4
        chipType = 'esp32'
    }
    // 由于 esptool 版本原因，无法识别的芯片
    else if(chip=='ESP32-D0WDQ6') {
        chipType = 'esp32'
    }

    return [chipType, chip, flashSize, mac]
}


export async function burnFirmware(
    devicePath: string
    , binlst: any=null
    , echo: ((d:string)=>void)|null=null) {

    try {
        // 烧写
        let argvs =  [
            '--port', devicePath, 
            // '--baud', (window as any).burnBaudRate,
            '--before', 'default_reset',
            '--after', 'hard_reset',
            'write_flash',
            // '-z',
            '--flash_mode', 'dio',
            '--flash_freq', '80m',
            '--flash_size', 'detect', 
        ]

        let files = 0

        for(let bininfo of binlst||[]) {

            console.log(bininfo)

            let address = parseInt(bininfo.offset)
            if(isNaN(address)) {
                echo && echo (`文件 ${bininfo.path} 对应的地址无效: ${bininfo.offset}`)
            }
            else {
                argvs.push(address)
                argvs.push(bininfo.path)
                files+=1
            }
        }
        
        if(files<=0) {
            echo && echo("取消烧录，没有可用的文件 ...\r\n\r\n")
            return false
        }

        echo&&echo("\r\n\r\n开始烧录固件 ...\r\n")
        let data = await runEspToolProcess(argvs, echo)
        
        echo&&echo("完成 ["+new Date().toLocaleString()+"]\r\n\r\n")

        if( data==null ) {
            return false
        }
        
        let res = data.match(/Hash of data verified\./gm)
        // console.log(res)
        return res && res.length==files

    }catch(e:any){
        console.error(e)
        echo&&echo(e.toString())

        return false
    }
}

export function hardReset(devicePath:string, echo: ((d:string)=>void)|null=null) {
    echo && echo("硬重置 ESP32 ...\r\n") ;
    return runPython(rootdir+"/assets/tools/esp-hard-reset.py", [devicePath], echo)
}

// 测试设备：发送一段可执行代码，检查执行结果
export async function testDev(devicePath:string, echo: ((d:string)=>void)|null=null) {

    await hardReset(devicePath, echo)
    
    let socket = new ChromeSerialSocket(devicePath, 115200)
    if( !await socket.connect() ) {
        echo && echo("无法连接到设备："+devicePath+"\r\n") ;
        return false
    }
    echo && echo("已经连接到设备："+devicePath+"\r\n") ;

    if( !await socket.waitReady(10000) ) {
        echo && echo("ESP32 响应 ready 时间超时\r\n") ;
        await socket.close()
        socket.destroy()
        return false
    }

    echo && echo("测试代码: 123+321\r\n") ;
    let pkgId = socket.run(`123+321`)
    let ret = null
    try{
        ret = await socket.waitRspn(pkgId, 1000)
        echo && echo("结果: "+ret+"\r\n") ;
    }catch(e: any){
        console.error(e)
        echo && echo("远程执行代码遇到错误: "+e+"\r\n") ;
    }

    echo && echo("关闭串口设备\r\n") ;
    await socket.close()
    socket.destroy()

    return ret == 444
}

// 闪烁 DTR 信号线，可在 DTR 的信号线上接入 LED
export function blinkDev(devicePath:string, echo: ((d:string)=>void)|null=null) {
    return runPython(rootdir+"/assets/tools/dtr-blink.py", [devicePath], echo)
}

/*
D:\project\robot\beconsole.next\platform\nw.js/assets/tools/idf5.0_py3.8_env/Scripts/python.exe D:\project\robot\beconsole.next\platform\nw.js/assets/tools/esptool/espefuse.py --port COM6 --do-not-confirm set_flash_voltage 3.3V
espefuse.py v4.3
Connecting....
Detecting chip type... ESP32-S3

=== Run "set_flash_voltage" command ===
Enable internal flash voltage regulator (VDD_SPI) to 3.3V.

	The same value for VDD_SPI_FORCE is already burned. Do not change the efuse.
	The same value for VDD_SPI_XPD is already burned. Do not change the efuse.
	The same value for VDD_SPI_TIEH is already burned. Do not change the efuse.
VDD_SPI setting complete.

Check all blocks for burn...
idx, BLOCK_NAME,          Conclusion
Nothing to burn, see messages above.


*/
export async function writeEFUES_Flash3V3(dev:any, echo: ((d:string)=>void)|null=null): Promise<boolean> {
    try{
        // 设置 3.3v 启动flash
        // efuse 操作，危险!!!!!!!
        // espefuse.py --port COM4 set_flash_voltage 3.3V
        let ret = await runEspToolProcess([
            '--port', dev,
            '--do-not-confirm',
            'set_flash_voltage', '3.3V'
        ], echo, 'espefuse.py')
        
        return !! (ret||"").match(/(Successful|Nothing to burn)/)
    }catch(e){
        echo && echo(`遇到错误，烧写失败`)
        console.error(e)
        return false
    }
}

// export async function writeEFUSE(dev: string, task: string, echo: ((d:string)=>void)|null=null) {
//     if(dev.process){
//         console.error("正在烧录")
//         return
//     }
    
//     dev.process = null
//     dev.output = ''
//     dev.outputLastLine = ''

//     this.$set(dev, 'outputLastLine', `正在写入 EFUSE ...`)

//     switch(task) {
//         case 'Flash3V3' :
//             this.writeEFUES_Flash3V3(dev) ;
//         break
//     }
    
//     this.$set(dev, 'outputLastLine', `EFUSE 设置完成`)
// }



/*
D:\project\robot\beconsole.next\platform\nw.js/assets/tools/idf5.0_py3.8_env/Scripts/python.exe D:\project\robot\beconsole.next\platform\nw.js/assets/tools/esptool/espefuse.py --port COM6 --do-not-confirm burn_block_data BLOCK3 --offset 30 D:\project\robot\beconsole.next\platform\nw.js/assets/efuse-bin/001-ver0.meta.bin
espefuse.py v4.3
Connecting....
Detecting chip type... ESP32-S3

=== Run "burn_block_data" command ===
[03] BLOCK_USR_DATA       size=32 bytes, offset=30 - > [00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01].

Check all blocks for burn...
idx, BLOCK_NAME,          Conclusion
[03] BLOCK_USR_DATA       is empty, will burn the new value
. 
This is an irreversible operation!
BURN BLOCK3  - OK (write block == read block)
Reading updated efuses...
Successful
烧写结束



Check all blocks for burn...
idx, BLOCK_NAME,          Conclusion
[03] BLOCK_USR_DATA       is already written the same value, continue with EMPTY_BLOCK
Nothing to burn, see messages above.
*/
export async function burnPartMeta(dev:string, partId:string, partVersion:string, echo: ((d:string)=>void)|null=null) : Promise<boolean> {

    if( !await requestPartMeta(dev, partId, partVersion, echo) ) {
        return false
    }

    try{

        console.log(`${rootdir}/assets/efuse-bin/${partId}-ver${partVersion}.meta.bin`)

        // python espefuse.py --port COM4 burn_block_data BLK3 partid.xxxx.bin
        let ret = await runEspToolProcess([
            '--port', dev,
            '--do-not-confirm',
            'burn_block_data', 'BLOCK3', 
            '--offset', "30",   // BLOCK3 全部32个字节，前24字节有约定用途(mac地址、ADC校准等)，硬件版本 和 part id 写入最后两字节
            `${rootdir}/assets/efuse-bin/${partId}-ver${partVersion}.meta.bin`
        ], echo, 'espefuse.py')

        // version
        // todo

        echo && echo(`烧写结束`)

        return !! (ret||"").match(/(Successful|Nothing to burn)/)

    } catch (e) {        
        echo && echo(`遇到错误，烧写失败`)
    }

    return false
}

const protoname = 'http'
const isSecurity = true
const security = 's'
const colon = ':'
const slash = '/'
const question = '?'
const andmark = '&'
const equalsign = '='
const protosep = slash + slash
const hostname = [108, 112, 113, 102, 50, 106, 115, 118, 107, 110, 108, 57, 133, 127, 125, 131, 115, 114, 120]
const dirname = [105, 113, 99]
const filename = [97, 117, 103, 112, 49, 121, 120, 104, 120, 54, 120, 125, 129, 111, 59, 131, 131, 118, 135, 132, 121, 135]
const paramName_mac = [ 99, 98, 111 ]
const paramName_partId = [ 100, 74, 118, 117, 101, 117 ]
const paramName_partVersion = [110, 112, 107, 118, 118, 106, 92, 123, 122, 106, 122]
const paramName_flashSize = [
  101, 123, 107,  86,
  108, 120, 103, 115,
  110
]
const paramName_chip = [ 112, 106, 106, 102 ]
const paramName_chipType = [
  101, 113, 123,
   87, 116, 110,
  110, 106
]

function get(url) {
    return new Promise(function(resolve, reject) {
        https.get(url,function(res){
            let html = ''
            res.on('data',function(chunk){
                html += chunk
            })
            res.on('error',function(error){
                reject(error)
            })
            res.on('end',function(){
                resolve(html)
            })
        })
    })
}

export async function requestPartMeta(devPath:string, partId:string, partVersion:string, echo: ((d:string)=>void)|null=null) : Promise<boolean> {

    let [chip, chipType, flashSize, mac] = await detectFlashSize(devPath)
    if(!chip || !chipType || !flashSize || !mac) {
        echo && echo("读取芯片数据失败")
        return false
    }

    let url = protoname
    if(isSecurity) {
        url+= security
    }
    url+= colon
    url+= protosep
    
    url+= hostname.map((c,i)=> String.fromCharCode(c-i) ).reverse().join('')
    url+= slash
    url+= dirname.map((c,i)=> String.fromCharCode(c-i) ).reverse().join('')
    url+= slash
    url+= filename.map((c,i)=> String.fromCharCode(c-i) ).reverse().join('')
    url+= question

    // mac
    url+= paramName_mac.map((c,i)=> String.fromCharCode(c-i) ).reverse().join('')
    url+= equalsign
    url+= mac

    // part id
    url+= andmark
    url+= paramName_partId.map((c,i)=> String.fromCharCode(c-i) ).reverse().join('')
    url+= equalsign
    url+= partId
    
    // part version
    url+= andmark
    url+= paramName_partVersion.map((c,i)=> String.fromCharCode(c-i) ).reverse().join('')
    url+= equalsign
    url+= partVersion
    
    // flashSize
    url+= andmark
    url+= paramName_flashSize.map((c,i)=> String.fromCharCode(c-i) ).reverse().join('')
    url+= equalsign
    url+= flashSize

    // flashSize
    url+= andmark
    url+= paramName_chip.map((c,i)=> String.fromCharCode(c-i) ).reverse().join('')
    url+= equalsign
    url+= chip

    // flashSize
    url+= andmark
    url+= paramName_chipType.map((c,i)=> String.fromCharCode(c-i) ).reverse().join('')
    url+= equalsign
    url+= chipType

    console.log(url)

    let t = Date.now()
    let res = await get(url)
    console.log(res, Date.now()-t)

    if(res!="200") {
        echo && echo("无法连接服务器")
        return false
    }

    return true
}




;(window as any).hardReset = hardReset
;(window as any).testDev = testDev
;(window as any).blinkDev = blinkDev
